Графика OpenGL
В этом разделе мы научимся создавать трехмерные изображения с помощью функций библиотеки OpenGL, для того чтобы в следующей главе разработать Windows-приложение, которое можно рассматривать как инструмент просмотра результатов научных расчетов. Материал этого раздела позволит вам постепенно войти в курс дела и овладеть очень привлекательной технологией создания и управления трехмерными изображениями. Сначала мы рассмотрим основные возможности библиотеки OpenGL, затем научимся управлять функциями OpenGL на примере простых приложений консольного типа и лишь после этого приступим к разработке Windows-приложения.
Обзор возможностей библиотеки OpenGL
Читатель, наверное, знает, что OpenGL это оптимизированная, высокопроизводительная графическая библиотека функций и типов данных для отображения двух-и трехмерной графики. Стандарт OpenGL был утвержден в 1992 г. Он основан на библиотеке IRIS GL, разработанной компанией Silicon Graphics (www.sgi.com). OpenGL поддерживают все платформы. Кроме того, OpenGL поддержана аппа-ратно. Существуют видеокарты с акселераторами и специализированные SD-кар-ты, которые выполняют примитивы OpenGL на аппаратном уровне.
Материал первой части этого урока навеян очень хорошей книгой (доступной в online-варианте) издательства Addison-Wesley «OpenGL Programming Guide, The Official Guide to Learning OpenGL». Если читатель владеет английским языком, то мы рекомендуем ее прочесть.
Microsoft-реализация
OpenGL включает полный набор команд OpenGL, то есть глобальных функций, входящих
в ядро библиотеки OPENGL32.LIB и имеющих префикс gl (например, glLineWidth).
Заметьте, что функции из ядра библиотеки имеют множество версий, что позволяет
задать желаемый параметр или настройку любым удобным вам способом. Посмотрите
справку по функциям из семейства glColor*. Оказывается, что задать текущий цвет
можно 32 способами. Например, функция:
void
glColorSb(GLbyte red, GLbyte
green, GLbyte blue);
определяет
цвет тремя компонентами типа GLbyte, а функция
void
glColor4dv(const GLdouble
*v) ;
задает его с помощью адреса массива из четырех компонентов.
С учетом этих
вариантов ядро библиотеки содержит более 300 команд. Кроме того, вы можете подключить
библиотеку утилит GLU32.LIB, которые дополняют основное ядро. Здесь есть функции
управления текстурами, преобразованием координат, генерацией сфер, цилиндров
и дисков, сплайновых аппроксимаций кривых
и поверхностей (NURBS — Non-Uniform Rational B-Spline), а также обработки ошибок.
Еще одна, дополнительная (auxiliary) библиотека GLAUX.LIB позволяет простым
способом создавать Windows-окна, изображать некоторые SD-объекты, обрабатывать
события ввода и управлять фоновым процессом. К сожалению, эта библиотека не
документирована. Компания Microsoft не рекомендует пользоваться ею для разработки
коммерческих проектов, так как она содержит код цикла обработки сообщений, в
который невозможно вставить обработку других произвольных сообщений.
Тип
GLbyte эквивалентен типу signed char, a GLdouble — типу double. Свои собственные
типы используются в целях упрощения переносимости на другие платформы. Список
типов OpenGL мы приведем ниже. Четвертый компонент цвета определяет прозрачность
цвета, то есть способ смешивания цвета фона с цветом изображения. Некоторые
команды OpenGL имеют в конце символ v, который указывает, что ее аргументом
должен быть адрес массива (вектора). Вектор в математике — это последовательность
чисел (координат), единственным образом задающих элемент векторного пространства.
Многие команды имеют несколько версий, позволяя в конечном счете задать вектор
разными способами.
Около двадцати Windows GDI-функций создано специально для работы с OpenGL. Большая часть из них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами функций с префиксом glx, которые подключают OpenGL к платформе X window System. Наконец, существует несколько Win32-функций для управления форматом пикселов и двойной буферизацией. Они применимы только для специализированных окон OpenGL.
К сожалению,
Microsoft-реализация OpenGL имеет ряд ограничений, которые не дают в полной
мере использовать всю мощь библиотек. Перечислим те из них, которые приведены
в документации MSDN.
Моделью или объектом в OpenGL называется структура в памяти, конструируемая из геометрических примитивов: точек, линий и полигонов, которые, в свою очередь, задаются своими вершинами (vertices). Из этих моделей OpenGL создает изображение в специально подготовленном окне. Процесс создания и демонстрации изображения называется передачей (rendering) изображения OpenGL. Конечным изображением является множество пикселов — мельчайших видимых элементов экранной поверхности. Информация о цвете пикселов размещена в памяти в виде битовых плоскостей (bitplanes). Так называется область памяти, которая содержит только один бит информации обо всех пикселах окна. В совокупности плоскости составляют буфер кадра (framebuffer), который содержит информацию, необходимую для того, чтобы дисплей отобразил все пикселы окна OpenGL.
OpenGL изображает графические примитивы (точки, сегменты линий или многоугольники), используя при этом множество независимо управляемых режимов (modes). Для задания примитивов, установки режимов и выполнения других операций необходимо вызывать функции OpenGL, или, как принято говорить, давать последовательность команд OpenGL Примитивы задаются своими вершинами, то есть точками трехмерного пространства. Кроме координат с каждой вершиной ассоциируются такие данные, как цвет, направление нормали (перпендикуляра), параметры текстуры и флаги границы (edge flags). Текстурами называются готовые bitmap-изображения, которые накладываются на многоугольники каркаса модели и вносят в нее эффект поверхности реального материала.
OpenGL — автомат с конечным числом состояний
OpenGL работает
по принципу конечного автомата, то есть автомата, который в каждый момент времени
находится в одном из состояний, принадлежащих конечному множеству допустимых
значений. В документации вы можете встретить в применении к OpenGL термины state
machine (конечный автомат) и assembly line (конвейер). Некоторые команды (вызовы
функций OpenGL) переводят автомат в различные состояния или режимы, которые
остаются неизменными до тех пор,
пока не придет следующая команда изменения состояния. Текущий цвет, как вы видели,
является одним из состояний. Другими состояниями являются:
Многие переменные, определяющие состояния, переключаются с помощью функций glEnable (включить) или gioisable (выключить). Каждая переменная состояния или режим имеет значение по умолчанию, и в любой точке программы вы можете узнать текущее состояние. Обычно для этой цели используется одна из 6-ТИ команд: glGetBooleanv, glGetDoublev, glGetFloatv, glGetlntegerv, glGetPointerv или glisEnabled. Выбор зависит от типа данных, которые задают состояние. Некоторые переменные состояния (state variables) заполняются более специфичными командами, например: glGetLight*, glGetError, glGetPolygonStipple. Множество состояний можно сохранить в стеке атрибутов командами glPushAttrib или glPushClientAttrib. Обычно так делают для того, чтобы временно изменить что-то, а затем восстановить состояния с помощью одной из команд: glPopAttrib, glPopClientAttrib.
Команды OpenGL
претерпевают одинаковый порядок обработки, проходя через последовательность
стадий, называемых конвейером обработки OpenGL (processing or rendering pipeline).
Схема конвейера приводится во многих источниках, приведем ее и мы (рис. 6.1),
для того чтобы не отсылать читателя к другим книгам. Ниже следует краткое описание
его основных блоков.
Рис. 6.1. Схема конвейера OpenGL
Списки команд OpenGL (Display Lists)
Все данные, описывающие геометрию или отдельные пикселы, могут быть сохранены в списках команд (display lists) для последующего использования. Альтернатива — немедленное использование (immediate mode). При вызове списка командой glCallList сохраненные данные из списка начинают двигаться по конвейеру так же, как и в режиме немедленного использования.
Вычислители (Evaluators)
Все геометрические примитивы описываются своими вершинами. Параметрические кривые и поверхности могут изначально-быть описаны контрольными точками или базовыми функциями (обычно полиномиальными). Вычислители — это методы, которые генерируют координаты вершин, нормали к поверхности, координаты текстур и цвета точек, опираясь на контрольные точки.
Сборка примитивов
На этом этапе происходит преобразование вершин в примитивы. Пространственные координаты (х, у, z) преобразовываются с помощью матриц размерностью (4 х 4). Основная цель — получить экранные, двухмерные координаты из трехмерных, мировых координат. Если включен режим генерации текстуры, то она создается на этом этапе. Освещенность вычисляется исходя из координат вектора нормали, расположения источников света, отражающих свойств материала, углов конусов света и параметров его аттенюации (ослабления). В результате получается цвет пиксела. Важным моментом на этапе сборки примитивов (primitive assembly) является отсечение (clipping), то есть удаление тех частей геометрии, которые попадают в невидимую часть пространства. Точечное отсечение пропускает или не пропускает вершину. Отсечение линий или полигонов подразумевает не только удаление вершин, но и возможное добавление некоторых (промежуточных) вершин. На этом этапе происходит учет перспективы, то есть уменьшение тех деталей сцены, которые расположены дальше от точки наблюдения, и увеличение тех деталей, которые расположены ближе. Здесь используется понятие видимого объема (viewport). Режим заполнения промежуточных точек полигона тоже играет роль на этапе сборки.
Операции с пикселами (Pixel Operations)
Данные о пикселах следуют в конвейере OpenGL параллельным путем. Данные, хранимые в массивах системной памяти, распаковываются с учетом набора возможных форматов, затем масштабируются, сдвигаются и обрабатываются так называемой картой пикселов (pixel map). Результат записывается либо в память текстуры, либо посылается на следующий этап — растеризацию. Отметьте, что возможна обратная операция считывания пикселов. При этом также Выполняются операции: масштабирование, сдвиг, преобразование и упаковка и помещение в системную память. Существуют специальные операции копирования данных из буфера кадра (framebuffer) в другую его часть или в буфер текстуры.
Сборка текстуры (Texture Assembly)
Текстуры — это bitmap-изображения, накладываемые на поверхности геометрических объектов для придания эффекта фактуры реального материала. Текстурные объекты создаются в OpenGL для упрощения их повторного использования. Использование текстур сопряжено с большими затратами, поэтому в работе с ними применяют специальные ресурсы, такие как texture memory. Так называют быструю видеопамять, приоритет использования которой отдается текстурным объектам.
Растеризация
Так называют преобразование как геометрических, так и данных о пикселах во фрагменты. Каждый фрагмент соответствует пикселу в буфере кадра. При вычислении цвета фрагмента учитывается большое количество факторов: узор штриховки полигона или линии, толщина линии и размер точки, сглаживание зубчатости линий, тень объекта, режим заполнения полигона, учет глубины изображения (факт видимости или невидимости) и др.
Операции с фрагментами
Каждая точка
уже двухмерного изображения характеризуется цветом, глубиной (значением координаты
Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется
фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если
он проходит пять тестов:
Кроме того,
фрагмент претерпевает другие изменения.
Для того чтобы
запомнить основные этапы обработки, повторим ключевые моменты.
Более подробную функциональную схему конвейера вы можете увидеть в разделе MSDN: Platform SDK/OpenGL/Overview/Introduction to OpenGL/OpenGL Processing Pipeline.
На примере
многочисленных хранителей экрана (screen-saver) вы видели, как гладко работает
анимация в OpenGL. OpenGL использует два буфера памяти (front and back). Первый
(front-буфер) отображается на экране, второй в это время может обрабатываться
процессором. Когда обработка закончится, то есть очередная сцена будет готова,
вы можете произвести быстрое переключение буферов (swap), обеспечивая тем самым
гладкую анимацию изображения. При обмене копирование массивов не происходит,
изменяется лишь значение указателя (адреса) отображаемого блока памяти. Отметьте,
что процесс рисования в back-буфер происходит быстрее, чем в front, так как
большинство видеокарт запрещают редактировать изображение в момент вертикальной
развертки, а это происходит 60-90 раз в секунду.
Рассмотрим основную схему алгоритма анимации, используемого в OpenGL-при-ложениях. В кино эффект движения достигается тем, что каждый кадр проецируется на экран в течение короткого промежутка времени, затем шторка проектора моментально закрывается, пленка продвигается на один кадр, вновь открывается шторка и цикл повторяется. Период цикла равен 1/24 с или даже 1/48 с в современных кинопроекторах. Современные компьютеры допускают смену кадра (refresh rate) до 120 раз в секунду. Рассмотренный алгоритм можно записать так.
в цикле по всем кадрам:
Если реализовать анимацию по такой схеме, то эффект будет тем более удручающим, чем ближе к 1/24 с подходит время создания изображения, так как полное изображение существует на экране лишь долю периода. Большую часть периода мы видим процесс рисования.
OpenGL предоставляет
возможность двойной буферизации (аппаратной или программной — зависит от видеокарты).
Алгоритм анимации в этом случае таков: пока проецируется первый кадр, создается
второй. Переключение кадров происходит только после того, как закончится формирование
второго кадра. Пользователь никогда не видит незавершенный кадр. Эту схему можно
представить в виде проектора с двумя кадрами пленки. В момент демонстрации первого
второй стирается и вновь рисуется. Новый алгоритм можно записать в цикле по
кадрам:
Последний
шаг алгоритма не начнет выполняться, пока не закончится предыдущий шаг — создание
нового кадра в back-буфере. Ожидание этого события (конец рисования в невидимый
буфер) дополняется ожиданием завершения цикла прямого хода развертки экрана.
Поэтому самая большая частота смены изображений равна текущему значению частоты
кадров дисплея. Допустим, что эта частота равна 60 Гц, тогда частота смены изображений
будет 60 fps (frames per second — кадров в секунду). Если время рисования занимает
немногим более 1/60 с (пусть 1/45 с), то один и тот же кадр будет проецироваться
два такта цикла развертки и частота смены изображений реально будет 30 fps.
Промежуток времени между 1/30 с и 1/45 с процессор простаивает (is idle). Если
время подготовки невидимого кадра нестабильно (плавает), то частота смены изображений
может измениться скачком, что воспринимается как неприятная помеха. Для сглаживания
этого эффекта иногда искусственно добавляют небольшую задержку, с тем чтобы
немного снизить частоту, но сделать ее стабильной. Отметьте, что OpenGL не имеет
команды переключения буферов, так как такая команда всегда зависит от платформы.
Мы будем пользоваться функцией SwapBuf f ers(HDC hdc), входящей в состав Windows
API.
Другие функции OpenGL позволяют размещать объекты на трехмерной сцене, выбирать точку размещения глаза наблюдателя (камеру), передвигать эту точку. Неотъемлемой частью трехмерной графики является освещение материалов. Конвейер OpenGL использует специальные алгоритмы подсчета цвета любого фрагмента с учетом заданных свойств материала и источников света. Моделирование атмосферных эффектов (тумана, дыма, дымки) делает изображения более реалистичными. Функции моделирования тумана, дыма, загрязнений или просто эффекта присутствия воздуха можно найти в справочной системе по ключевому слову Fog.
Механизм anti-aliasing сглаживает неровные края линий, отображаемых на компьютерном дисплее при низком графическом разрешении. Anti-aliasing является стандартной техникой в компьютерной графике и заключается в изменении цвета точек вблизи границ изломов. Техника Gouraud-тени сглаживает тень трехмерного объекта для достижения тонких различий цветов на специфической поверхности.
Четвертая
составляющая цветового кода RGBA носит название alpha. Alpha-смешивание позволяет
комбинировать цвет обрабатываемого фрагмента с цветом точек, которые уже хранятся
в буфере, моделируя тем самым прозрачность воздуха, стекла или другого материала.
Этот эффект используют при демонстрации распределения поля внутри замкнутого
объема. Достаточно дать пользователю возможность управлять степенью прозрачности
границ, и он сможет рассматривать результаты вычислений (например, поверхности
равного уровня искомого поля) внутри трехмерных объектов.
OpenGL не
предоставляет разработчику команд для описания сложных моделей. В ней есть примитивные
геометрические объекты: точки, линии и многоугольники. Разработчик должен сам
сконструировать модели, основываясь на этих нескольких простых примитивах. Но
есть библиотеки, например классы Open Inventor, которые реализуют более сложные
модели. Разработчик может использовать эти библиотеки для построения своих собственных.
Конвейер OpenGL реализует процедурный, а не описательный подход к созданию изображения. Он выполняет функцию сервера, который обслуживает клиента — приложение, генерирующее последовательность команд. Коды сервера могут выполняться на другом компьютере, сервер может одновременно поддерживать несколько контекстов передачи OpenGL, а клиент может подключаться к любому из них. Однако система Windows полностью контролирует память, отводимую под буфер кадра (frame buffer). OpenGL может вступить в игру только после того, как Windows создаст и подготовит окно для передачи (rendering) изображения OpenGL.
Окно OpenGL
имеет свой собственный формат пикселов. Необходимым условием ее работы является
установка pixel-формата экранной поверхности в контексте устройства HDC, а следовательно,
и в окне, с которым он связан. Формат устанавливается один раз, повторная установка
недопустима, так как может привести к сбоям в работе подсистемы управления окнами
(Windows Manager). После установки формата (вызов SetPixelFormat) следует создать
контекст передачи изображения OpenGL, описатель которой имеет тип HGLRC. Контекст
передачи (rendering context) создается функцией wglCreateContext с учетом выбранного
формата пикселов. Контекст передачи изображения — это связь OpenGL с Windows.
Создание этого контекста требует, чтобы обычный контекст существовал и был явно
указан в параметре wglCreateContext. Контекст HGLRC использует тот же формат
пикселов, что и HDC.
Несмотря на
сходство, эти контексты различны. HDC содержит информацию относящуюся к функциям
GDI, a HGLRC — к функциям OpenGL. Поток, вызывающий функции OpenGL, должен предварительно
объявить контекст передачи текущим (current). Иначе вызовы не будут иметь эффекта.
Уничтожать контекст передачи надо после отсоединения его от потока. Несколько
контекстов передачи могут одновременно рисовать в окне OpenGL, но только один
из них (тот, который ассоциирован с HDC) может быть текущим или активным в потоке.
Для описания
формата пикселов экранной поверхности в OpenGL используется структура PIXELFORMATDESCRIPTOR.
Прежде всего pixel format — это описание цветового режима, действующего в данном
окне. Например, если видеокарта может работать в режиме передачи 256 цветов,
то для кодирования цвета каждого пиксела в этом режиме необходимо иметь 8 бит
памяти. В этом случае говорят о 8-битовой глубине поверхности рисования или
окна OpenGL. Иногда в таком случае говорят о 8-битовой глубине цвета. Существуют
режимы с 15-битовой глубиной (32 768 цветов), 16-битовой (65 536 цветов), 24-битовой
(16 миллионов цветов). Выбор формата зависит от возможностей карты и намерений
разработчика. Кроме глубины цвета к pixel-формату относятся такие настройки,
как:
Вы можете
выбрать один из более чем 20 готовых pixel-форматов или задать произвольную
комбинацию параметров и попросить найти ближайшую возможную ее реализацию. Microsoft
GDI-реализация OpenGL вносит свои коррективы в возможные варианты реализации
pixel-формата, а аппаратная поддержка меняется в зависимости от производителя
и может значительно расширить его возможности. Каждое окно OpenGL должно иметь
свой собственный pixel-формат.
По умолчанию Windows не вырезает (в смысле перерисовки) дочерние окна из клиентской области родительского окна, поэтому при создании окнам OpenGL следует задать бит стиля WS_CLIPCHILDREN. В этом случае система не позволяет рисовать родительскому GDI-окну в пределах дочернего окна OpenGL. Несколько окон OpenGL, каждое со своим форматом, могут быть отображены одновременно, поэтому необходимо установить еще один бит стиля WS_CLIPSIBLINGS, чтобы предотвратить рисование в окне соседа. Для окон OpenGL недопустим стиль CS_PARENTDC (CM. MSDN).
Подготовку
контекста передачи OpenGL надо рассматривать как некий обязательный ритуал,
в котором порядок действий давно определен и без которого нельзя начинать творческую
работу по созданию сцены OpenGL. Стоит где-то промахнуться, и вы увидите молчаливый
белый экран. Сначала надо подготовить окно так, чтобы вызовы функций OpenGL
начали работать. В этой процедуре выделяют следующие шаги:
Чтобы использовать функции библиотеки OpenGL в вашем приложении, надо убедиться, что в системном каталоге Windows присутствуют модули OpenGL32.dll и GLU32.dll. Они должны быть там, так как компания Silicon Graphics (авторы пакета OpenGL) постаралась, чтобы поддержка OpenGL на платформе Windows была максимально доступна и достаточно надежна. Однако хочу предупредить, что я встречал системы, в которых контекст передачи (rendering context) OpenGL работает ненадежно — появляются пятна пробелов и задержка перерисовки, если работа идет не в полноэкранном режиме. Если это есть, то должно проявляться при запуске любой программы, использующей OpenGL. Причина, видимо, в драйвере видеопамяти.
Типы данных
OpenGL использует
свои собственные типы данных, которые должны соответствовать аналогичным типам
той платформы, на которой библиотека установлена. В Microsoft-реализации соответствие
типов задано в файле заголовков GL.H так, как показано ниже. Эта таблица понадобится
вам при анализе примеров и при разработке собственного кода:
typedef unsigned int GLenum;
typedef unsigned char GLboolean;
typedef unsigned int GLbitfield;
typedef signed char GLbyte;
typedef short GLshort;
typedef int GLint;
typedef int GLsizei;
typedef unsigned char GLubyte;
typedef unsigned short GLushort;
typedef unsigned int GLuint;
typedef float GLfloat;
typedef float
GLclampf;
typedef double GLdouble;
typedef double GLclampd;
typedef void
GLvoid;
Для исследования
возможностей функций библиотек OpenGL целесообразно создать простой проект консольного
типа, в котором для работы с другим (Windows) окном будут использованы функции
дополнительной библиотеки OpenGL, описанной в файле GLAUX.LIB. Рассмотрим последовательность
шагов для создания нового проекта консольного типа.
Далее вы будете
вводить код в окно редактора Studio.Net (вкладка OG.cpp). Для того чтобы компоновщик
подключил все библиотеки OpenGL, произведите настройку проекта.
В новый пустой файл OG.cpp поместите следующий код приложения, которое для создания Windows-окна пользуется услугами библиотеки GLAUX.LIB. Для этого необходимо к проекту консольного типа подключить файл Windows.h1:
#include <Windows.h>
#include
<math.h>
//====== Подключение
заголовков библиотек OpenGL
#include
<GL\gl.h>
# include <GL\glu.h>
#include <GL\Glaux.h>
//=====Макроподстановка для изображения одной линии
#define Line(xl,yl,x2,y2) \
glBegin(GL_LINES); \
glVertex2d ( (xl), (yl)); \
glVertex2d ((x2),(y2)); \
glEnd() ;
//====== Реакция
на сообщение WM_PAINT
void _stdcall OnDraw()
{
//====== Стираем
буфер кадра (framebuffer)
glClear (GL_COLOR__BUFFER_BIT)
;
//====== Выбираем
черный цвет рисования
glColorSf (0.,
0., 0.);
//=== В 1-м ряду рисуем 3 линии с разной штриховкой
glEnable (GL_LINE_STIPPLE);
glLineWidth (2.);
glLineStipple
(1, 0x0101); // dot
Line (50., 125.,
150., 125.);
glLineStipple
(1, OxOOFF); // dash
Line (150., 125.,
250., 125.);
glLineStipple
(1, OxlC47); // dash/dot/dash
Line (250., 125.,
350., 125.);
//====== Во 2-м
ряду то же, но шире в 6 раз
glLineWidth (6.);
glLineStipple
(1, 0x0101); // dot
Line (50., 100.,
150., 100.);
glLineStipple
(1, OxOOFF); // dash
Line (150., 100.,
250., 100.);
glLineStipple
(1, OxlC47); // dash/dot/dash
Line (250., 100.,
350., 100.);
//== Во 3-м ряду
7 линий являются частями
//== полосы (strip).
Учетверенный узор не прерывается
glLineWidth (2.);
glLineStipple
(4, OxlC47); // dash/dot/dash
glBegin (GL_LINE_STRIP);
for (int
i =1; i < 8; i++)
glVertex2d (50.*i,
75.); glEnd() ;
//== Во 4-м ряду 6 независимых, отдельных линий
//== Тот же узор, но он каждый раз начинается заново
for (i = 1; i < 7; i++)
{
Line (50*1, 50, 50* (i+1), 50);
}
//====== во 5-м
ряду 1 линия с тем же узором
glLineStipple (4, OxlC47); // dash/dot/dash
Line (50., 25.,
350., 25.);
glDisable (GL_LINE_STIPPLE); glFlush ();
}
//===== Реакция
на WM_SIZE
void _stdcall
OnSize (int w, int h)
{
glViewport (0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode
(GL_PROJECTION); glLoadldentity();
//====== Режим
ортографической проекции
gluOrtho2D (0.0, double(w), 0.0, double(h));
}
//====== Настройки
void Init()
{
//====== Цвет
фона - белый
glClearColor
(1., 1., 1., 0.);
//====== Нет
интерполяции цвета при растеризации
glShadeModel
(GL_FLAT); }
void main()
{
//=== Установка
pixel-формата и подготовка окна OpenGL
auxInitDisplayMode
(AUX_SINGLE | AUX_RGB);
auxInitPosition
(200, 200, 550, 250);
auxInitWindow("My
Stipple Test");
Init() ;
auxReshapeFunc (OnSize);
// Кого вызывать при WM_SIZE auxMainLoop(OnDraw);
// Кого вызывать при WM_PAINT
}
Функция main
содержит стандартную последовательность действий, которые производятся во всех
консольных приложениях OpenGL. С ней надо работать как с шаблоном приложений
рассматриваемого типа. Первые три строчки функции main устанавливают pixel-формат
окна OpenGL. Заботу о его выборе взяла на себя функция auxInitDisplayMode из
вспомогательной библиотеки. В параметре мы указали режим использования только
одного (front) буфера (бит AUX_SINGLE) и цветовую схему без использования палитры
(бит AUX_RGB).
В функции
init обычно производят индивидуальные настройки конечного автомата OpenGL. Здесь
мы установили белый цвет в качестве цвета стирания или фона окна и режим заполнения
внутренних точек полигонов. Константа GL_FLAT соответствует отсутствию интерполяции
цветов. Вызов функции auxReshapeFunc выполняет ту же роль, что и макрос ON_WM_SIZE
в MFC-приложении. Происходит связывание сообщения Windows с функцией его обработки.
Все функции обработки
должны иметь тип void _stdcall. Вы можете встретить и эквивалентное описание
этого типа (void CALLBACK). Имена функций OnDraw и OnSize выбраны намеренно,
чтобы напомнить о Windows и MFC. В общем случае они могут быть произвольными.
Важно запомнить, что последним в функции main должен быть вызов auxMainLoop.
В OnSize производится
вызов функции glviewport, которая задает так называемый прямоугольник просмотра.
Мы задали его равным всей клиентской области окна. Конвейер OpenGL использует
эти установки так, чтобы поместить изображение в центр окна и растянуть или
сжать его пропорционально размерам окна. Аффинные преобразования координат производятся
по формулам:
Xw=(X+1)(width/2)+X0
Yw=(Y+1)(height/2)+Y0
В левой части
равенств стоят оконные координаты:
Как видно
из подстановки в формулу, точка с координатами (0,0) попадет в центр окна, а
при увеличении ширины или высоты окна (width или height) координаты изображения
будут увеличиваться пропорционально. Вызов
glMatrixMode
(GL_PROJECTION);
определяет
в качестве текущей матрицу проецирования, а вызов glLoadldentity делает ее равной
единичной матрице. Следующий за этим вызов
gluOrtho2D (0.0,
double(w), 0.0, double(h));
задает в качестве матрицы преобразования матрицу двухмерной ортографической (или параллельной) проекции. Изображение будет отсекаться конвейером OpenGL, если его детали вылезают из границ, заданных параметрами функции gluOrtho2D.
Основные действия разворачиваются в функции перерисовки. Здесь мы рисуем несколько линий, изменяя узор их штриховки. Режим штриховки линий включается командой glEnable (GL_LINE_STIPPLE). Узор штриховки задается параметрами функции glLineStipple. Первый параметр является коэффициентом повторения, а второй определяет сам узор. Он должен быть 16-битовой константой или переменной, последовательность бит которой определяет последовательность фрагментов при растеризации линии. Порядок использования битов возрастающий, то есть нулевой бит используется первым. Каждому пикселу соответствует один бит, что характеризует ситуацию: текущий цвет либо включен (бит равен 1), либо выключен (бит равен 0). Алгоритм станет очевидным, если вы запустите приложение (Ctrl+F5), устраните возможные ошибки и увидите результат. Обратите внимание на различие штриховки в 3-й и 4-й строках. Попытайтесь объяснить различие. Создайте несколько своих собственных узоров штриховых (stippled) линий.
Теперь применим штриховку (stipple) к полигонам. Режим штриховки включается и выключается стандартным способом:
glEnable (GL_POLYGON_STIPPLE) ;
glDisable (GL_POLYGON_STIPPLE);
Bitmap-узор
(pattern) штриховки надо предварительно подготовить в массиве такой размерности,
чтобы заполнить bitmap площадью 32x32 = 1024 пиксела. Размерность массива с
узором определяется так: 1024 бита можно разместить в 128 переменных по одному
байту. Мы разместим их в 16 строках по 8 байт. Имеем 16 х х 8 х 8 = 1024 бита
(или пиксела).
Массивы объявлены
глобально. Адреса массивов с узором подаются на вход функции glPolygonStipple:
GLubyte gSpade[] = // Узор - пики
{
0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0xlf, 0xff, Oxff,
0xf8, 0xlf, 0x00, 0x00, 0xf8,
0x01, OxcO, 0x03,
0x80, 0x00, 0x70, 0xOe, 0x00,
0x00, |
0x20, |
0x04, |
0x00, |
0x00, |
0x30 |
, 0x0c, |
0x00, |
||
0x00, |
0x10, |
0x08, |
0x00, |
0x00, |
0x18 |
, 0x18, |
0x00, |
||
0x07, |
0хс4, |
0x23, |
0xe0, |
0x0f, |
0xf8 |
, 0xlf, |
0xf0, |
||
0x38, |
0xlc, |
0x38, |
0xlc, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
0x30, |
0x00, |
0x00, |
0x0c, |
0x18, |
0x00 |
, 0x00, |
0x18, |
||
0х0е, |
0x00, |
0x00, |
0x70, |
0x03, |
0x00 |
, 0x00, |
0xc0, |
||
0x00, |
OxcO, |
0x03, |
0x00, |
0x00, |
0x70 |
, 0x0e, |
0x00, |
||
0x00, |
0x18, |
0x18, |
0x00, |
0x00, |
0x0c |
, 0x30, |
0x00, |
||
0x00, |
0x07, |
OxeO, |
0x00, |
0x00, |
0x03 |
, 0xc0, |
0x00, |
||
0x00, |
0x01, |
0x80, |
0x00, |
0x00, |
0x00 |
, 0x00, |
0x00 |
||
GLubyte |
gStripU = |
// Другой узор
- |
полоса |
|
|||||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
Функцию OnDraw
замените целиком. Так же поступайте и дальше. Следите лишь за изменениями в
функциях main, OnSize и init, которые понадобятся при радикальной смене режима
передачи (rendering). Позже мы перейдем к передаче трехмерной графики, а пока
режим тот же — двухмерная графика:
void _stdcall OnDraw()
{
//====== Стираем
окно
glClear (GL_COLOR_BUFFER_BIT);
//====== Цвет
фона (синеватый)
glColor3f (0.3f,
0.3f, 1.);
//== Рисуем сначала unstippled rectangle (без узора)
//== Rect - это тоже полигон
glRectf (20.,
20., 115., 120.);
glColor3f (1.,
0., 0.); // Меняем цвет на красный
glEnable (GL_POLYGON_STIPPLE);
// Включаем штриховку
glPolygonStipple
(gStrip); // Задаем узор
glRectf (120.,
20., 215., 120.); // Рисуем
glColorSf (O.,0.,0.); // Меняем цвет на черный
glPolygonStipple (gSpade);
// Меняем узор
glRectf (220., 20., 315., 120.);
glPolygonStipple (gStrip); // Меняем узор
glColor3f (0., 0.6f, 0.3f);
glRectf (320.,
20., 415., 120.);
//== Готовимся заполнить более сложный, невыпуклый
//== (nоn convex) полигон
glPolygonStipple (gSpade);
glColorSd (0.6,
O.f, 0.3f);
//======= Шесть
вершин по три координаты
float c[6][3]
=
{
420.,120.,0.,
420.,70.,0.,
470.,20.,0.,
520., 70.,0.,
520.,120.,0.,
470.,100.,0.
};
//== Здесь мы специально выбираем nоn convex полигон,
//== чтобы увидеть как плохо с ним обходится OpenGL
glBegin (GL_POLYGON)
;
for (int i=0; i<6; i++)
glVertex3fv(c[i] ) ;
glEnd() ;
glDisable (GL_POLYGON_STIPPLE) ;
glFlush ();
}
Запустите
и убедитесь в том, что последний полигон потерял одну точку. Затем замените
цикл задания его вершин на:
for (int
i=5; i>=0; i--) glVertex3fv(c[i]) ;
Здесь мы изменили
порядок обхода вершин и начали с вогнутой вершины. Запустите и убедитесь в том,
что теперь в полигоне есть все шесть вершин. OpenGL не гарантирует точную передачу
вогнутых полигонов. Поэтому для надежной передачи их надо предварительно разбивать
на выпуклые части. Если этими частями будут треугольники, то процесс разбиения
называется tessellation (мозаика). Есть специальные функции для тесселяции полигонов.
Их рассмотрение выходит за рамки этой книги. Попробуйте самостоятельно задать
рассмотренный выше полигон в виде двух выпуклых четырехугольников. Для этого
посмотрите справку по функции glBegin с параметром GL_QUADS.
Полигоны можно
рисовать либо закрашенными (режим — GL_FILL), либо в скелетном виде (GL_LINE),
либо в виде намеков (GL_POINT). Испробуйте все режимы
на примере невыпуклой звезды. При рисовании точками попробуйте предварительно
дать команду glPointSize (5):
void _stdcall OnDrawf)
{
glClear (GL_COLOR_BUFFER_BIT);
glColor3d (1.,
0.4, 1.);
//=== 2 угла, характеризующие звезду и
//=== 2 характерные точки
double pi
= 4. * atan(l.),
al = pi / 10.,
a2 = 3. * al,
xl = costal),
yl = sin(al)',
x2 = cos(a2),
y2 = sin(a2);
//=== Мировые
координаты вершин нормированной звезды
double с[5][3]
=
{
0., 1., 0.,
-х2, -у2, 0.,
xl, yl,
0.,
-xl, yl, 0.,
х2, -у2, 0.,
};
//====== Оконные
координаты
for (int i=0; i<5; i+t)
{
c[i][0] = 250
+ 100*c[i][0];
c[i][l] = 100 + 100*c[i] [1] ;
}
//=== Режим заполнения полигона - скелетный
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//=== Задаем вершины полигона
glBegin(GL_POLYGON);
for (i=0;
i<5; i++)
glVertex3dv(c[i] ) ;
glEnd() ;
glFlush() ;
}
Каждой вершине
по умолчанию присваивается булевский признак (флаг) того, что из нее может выходить
видимое ребро (линия). Если надо отменить рисование линии, например скрыть тесселяцию
вогнутого полигона, то можно снять флаг ребра командой glEdgeFlag(GL_FALSE);
для текущей вершины. Затем можно вновь установить его, когда дело дойдет до
ребер, которые должны быть видимы — команда glEdgeFlag(GL_TRUE);. Попробуйте
самостоятельно вставить флаги ребер в следующем фрагменте программы так, чтобы
скрыть линию соединения
двух четырехугольников. Попробуйте затем заменить алгоритм так, чтобы изобразить
ту же фигуру в виде одного полигона:
void
_stdcall OnDraw()
{
glClear (GL_COLOR_BOFFER_BIT);
glColorSd (1., 0.4, 1.);
//====== Вогнутый
шестиугольник, но мы зададим его
//====== в виде
двух четырехугольников
float с[6][3] =
{
200. 200.,0.,
200. 100.,0.,
250. 20.,0.,
300. 100.,0.,
300. 200.,0.,
250. 100.,0.,
};
glPolygonMode(GL_FRONT_AND_BACK,
GL_LINE);
glBegin(GL_QUADS);
glVertex3fv(c[5])
glVertex3fv(c[0])
glVertex3fv(c[l])
glVertex3fv(c[2])
glVertex3fv(c[5])
glVertex3fv(c[2])
glVertex3fv(c[3])
glVertex3fv(c[4])
glEnd();
glFlush ();
}
В ортографической
проекции .(giuOrtho2D) мы, в сущности, создавали двухмерные изображения в плоскости
z = 0. В других типах проекций (gldrtho и gluPerspective) можно создавать трехмерные
изображения. Эффект реального трехмерного пространства достигается в проекции
с учетом перспективы. Теперь мы будем пользоваться только этим режимом передачи.
Другой режим glOrtho вы опробуете самостоятельно, так как я не вижу какой-либо
интересной сферы его применения. Вставьте в обработчик WM_SIZE вместо строки:
gluOrtho2D (0.,
double (w), 0., double(h) ) ;
строку:
gluPerspective(45.,
double(w)/double(h), 1., 100.);
В OpenGL для
обозначения видимого объема используется термин frustum. Он имеет латинское
происхождение и примерно означает «отломанная часть, кусок».
Frustum задается
шестью плоскими границами типа (min, max) для каждой из трех пространственных
координат. В перспективном режиме просмотра frustum — это усеченная пирамида,
направленная на наблюдателя из глубины экрана. Все детали сцены, которые попадают
внутрь этого объема, видны, а те, что выходят за него, — отсекаются конвейером
OpenGL. Другой режим просмотра — ортографический, или режим параллельной проекции,
задается с помощью функции glOrtho. Он не учитывает перспективу, то есть при
увеличении (удалении) координаты Z объекта от точки, в которой располагается
наблюдатель, размеры объектов и углы между ними не изменяются, что напоминает
плоские проекции объекта. Первый параметр функции gluPerspective задает угол
перспективы (угол обзора). Чем он меньше, тем больше увеличение. Вспомните школьные
объяснения работы телескопа или бинокля, где были настройки фокусного расстояния,
определяющего угол зрения. Последние два параметра задают переднюю и заднюю
грани видимого объема или frustum'a. Он определяет замкнутый объем, за пределами
которого отсекаются все элементы изображения. Смотри иллюстрации в MSDN / Periodicals
/ Periodicals 96 / Microsoft System Journals/November / OpenGL Without the Pain.
Боковые грани фрустума определяются с учетом дисбаланса двух размеров окна (отношения
double(w) / double(h)). Мы вычисляем его и подаем на вход функции в качестве
второго параметра.
Вспомните
и найдите функцию, в которой мы задавали размеры окна, и увеличьте вертикальный
размер до 500, так как далее мы собираемся изображать более крупные объекты.
Введите определения новых глобальных переменных:
//====== Углы
поворотов изображения вокруг осей X и Y
double gdAngleX,
gdAngleY; //====== Сдвиги вдоль координат
double gdTransX,
gdTransY, gdTransZ = -4.;
С их помощью
мы будем транслировать (перемещать) изображения в трехмерном пространстве и
вращать их вокруг двух осей. Включите учет глубины, вставив вызов
glEnable(GL_DEPTH_TEST);
в функцию Init.
Туда же вставьте установку режима заполнения полигонов
glPolygonMode(GL_FRONT_AND_BACK,
GL_FILL);
и уберите строку,
задающую текущий цвет вершин
glColorSd (1.,
0.4, 1.);
так как мы
теперь будем задавать его в другом месте. При подготовке окна OpenGL и формата
его пикселов надо установить бит AUX_DEPTH — учет буфера глубины. Замените существующий
вызов функции auxlnitDisplayMode на: auxInitDisplayMode
(AOX_SINGLE I AUX_RGB I AUX_DEPTH);
В функции
перерисовки, приведенной ниже, мы создадим куб, координаты которого будем преобразовывать
с помощью матрицы моделирования. Порядок работы с этой матрицей таков:
Предположим,
например, что текущая (current) матрица С размерностью 4x4 равна единичной С
= 1 и поступает команда glTranslated (dx, dy, dz);. Эта команда создает матрицу
сдвига Т и умножает ее справа на текущую (единичную) матрицу (С = I*Т). Затем
она вновь записывает результат в текущую матрицу С. Теперь текущая матрица приняла
вид:
1
|
0
|
0
|
dx
|
||
C=
|
0
|
1
|
0
|
dy
|
|
0
|
0
|
1
|
dz
|
||
0
|
0
|
0
|
1
|
Если после
этого дать команду glVertexSd (x, у, z); то координаты точки (х, у, z) преобразуются
по правилам умножения матрицы на вектор:
1
|
0
|
0||
|
dx
|
||
|
x|
|
|x
|
+dx|
|
||
0
|
1
|
0||
|
dy
|
||
|
y|
|
=
|
|y
|
+dy|
|
|
0
|
0
|
1||
|
dz
|
||
|
z|
|
|z
|
+dz|
|
||
0
|
0
|
0||
|
1
|
||
|
1|
|
|1
|
Вы
должны помнить, что вершины всех примитивов в OpenGL заданы 4-ком-— понентным
вектором (х, у, z, w). По умолчанию нормирующий компонент w-1. При работе с
двухмерными изображениями мы для всех вершин задаем координату z = 0. Обратите
внимание на то, как четвертый компонент w помогает производить преобразования,
в нашем случае сдвиг, а команда glTranslate* учитывает координаты сдвигов вдольтрех
пространственных осей (dx, dy, dz).
Команды вращения
glRotate* и растяжения-сжатия glScale* действуют сходным образом. В функции
onDraw, приведенной ниже, начальный поворот и последующие вращения вокруг оси
Y осуществляются вызовом glRotated (gdAngleY, 0., l., 0.);. Аналогичный вызов
glRotated (gdAngleX, 1., 0., 0.); вращает все точки примитивов вокруг оси X:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BOFFER_BIT
I GL_DEPTH_BUFFER_BIT);
//== Будем пользоваться услугами матрицы моделирования glMatrixMode <GL_MODELVIEW);
glLoadldentity
();
//=== Задаем
смещение координат точек будущих примитивов glTranslated(gdTransX, gdTransY,
gdTransZ);
//===Задаем вращение координат точек будущих примитивов
glRotated(gdAngleY, 0.,1.,0.);
glRotated(gdAngleX,
1.,0.,0.);
//====== Координаты
точек куба (центр его в нуле)
static float
v[8][3] =
{
-1, 1.,-1., //4 точки задней грани задаются
1., 1., -1., //в порядке против часовой стрелки
1-, -1-, -1.,
-1, -1., -1.,
-1, 1,, 1., //4
фронтальные точки
-1-, -1., 1.,
1, -1., 1.,
1, 1., 1.
};
//====== 6 нормалей
для 6-ти граней куба
static double
norm[6][3] =
{
0., 0., -1., // Rear
0., 0., 1., // Front
-1., 0., 0., // Left
1., 0., 0., // Right
0., 1., 0., // Top
0., -1., 0. //
Bottom
};
//====== Индексы
вершин
static GLuint id[6][4] =
{
0,1,2,3,// Rear
(обход CCW - counterclockwise)
4,5,6,7, // Front
0,3,5,4, // Left
7,6,2,1, // Right
0,4,7,1, // Top
5,3,2, 6, //
Bottom
};
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColorSd (1., 0.4, 1.);
glBegin(GL_QUADS);
//====== Долго
готовились - быстро рисуем
for (int
i = 0; i < 6; i++)
{
glNormal3dv(norm[i])
;
for (int
j = 0; j < 4; j++)
glVertex3fv(v[id[i] [j]]);
}
glEndf) ;
glFlush ();
}
Запустите и отладьте приложение. Вы должны увидеть совсем плоский квадрат, несмотря на обещанную трехмерность объекта. Пока ничего вразумительного, никакого трехмерного эффекта. Закомментируйте или удалите (или измените на GL_SMOOTH) настройку glShadeModel (GL_FLAT), так как теперь мы хотим интерполировать цвета при изображении полигонов. Это работает при задании разных цветов вершинам. Попробуйте задать всем вершинам разные цвета.
Попробуйте покрутить изображение, изменяя значения переменных gdAngleX, gdAngleY. Например, вместо нулевых значений, присваиваемых глобальным переменным по умолчанию, задайте:
double
gdAngleX=20, gdAngleY=20;
Посмотрите в справке смысл всех параметров функции glRotated и опробуйте одновременное вращение вокруг двух осей, задав большее число единиц в качестве параметров. Позже мы автоматизируем процесс сдвигов и вращений, а сейчас, пока мы не умеем реагировать на сообщения мыши, просто измените значение какого-либо угла поворота и запустите музыку. Объясните результаты. Попробуйте отодвинуть изображение, изменив регулировку gdTransZ. Объясните знак смещения.
Пока нет освещения,
все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по
нормали (перпендикуляру) к поверхности. Однако в OpenGL нормаль надо задавать
в вершинах, так как в случае произвольной криволинейной поверхности направление
нормали различно в каждой ее точке. Чем точнее вычислен вектор нормали, тем
реалистичней изображение. Но это дело довольно тонкое. Для тех, кто не любит
математику, то есть излишне напрягать свое мышление, — просто отвратительное.
Примеры с автонормалями расслабляют и усыпляют бдительность, так как они скрывают
детали реализации. Чтобы с ними работать, тоже надо прилагать усилия и правильно
включать вычислители (evaluators). Смотри документацию по функциям giMap*. В
нашем же случае все просто. Нормали уже вычислены, осталось включить свет. Сделайте
это, вставив изменения в тело функции init. Включите еще два параметра в конечном
автомате (state machine) OpenGL.
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
Задайте некоторый
поворот, например double gdAngleX=15, gdAngleY=30, и запустите на выполнение.
Изображение должно стать значительно лучше, но куда делся цвет куба? Свет исключил
цвет. Дело в том, что теперь цвет каждого пиксела вычисляется по формуле, которая
учитывает цвет материала поверхности, его отражающие и испускающие свойства,
цвет самого света, его направление и законы распространения (точнее, затухания
— attenuation). По умолчанию OpenGL учитывает только направление света, но не
место расположения источника. По умолчанию же свет направлен вдоль оси Z. Обратите
внимание на то, что индекс 0 в GL_LIGHTO означает, что мы включаем первый из
GL_MAX_LIGHTS возможных источников света. Эта константа зависит от платформы.
Давайте определим ее для нашей платформы. Вставьте такой фрагмент:
int Lights;
glGetIntegerv(GL_MAX_LIGHTS, &Lights);
_asm
nор
внутрь функции Init (после строки glEnable(GL_LIGHTO);) и поставьте точку останова
(F9) на строке __asm пор.
Ассемблерная
вставка _asm пор упрощает просмотр значения переменных в окне Variables, так
как не дает новых (и отвлекающих) элементов просмотра. Идея использования такого
приема принадлежит Марине Полубенцевой, с которой мы сотрудничаем в Microsoft
Authorized Education Center при ФПК СПбГТУ (www.Avalon.ru). В книге использовано
еще несколько идей и технологических приемов, автором которых является Полубенцева.
Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите
в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите,
то используйте описанный прием в дальнейшем для выяснения многочисленных параметров
и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление
о количестве этих параметров. Теперь уберите отладочный код и включите еще один
тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте
строку:
glEnable(GL_COLOR_MATERIAL) ;
в функцию Init и запустите приложение. Обратите внимание на отличие оттенков
цвета разных граней. Они определяются OpenGL с учетом направления нормалей.
Попробуйте изменить их направление и посмотрите, что получится.
Интерактивное управление положением и ориентацией
Теперь хочется рассмотреть трехмерный объект с разных сторон. Удобнее это делать с помощью мыши. Документация MSDN содержит весьма скудные сведения относительно aux-функций, но в примерах все же можно найти какую-то информацию. Оказывается для введения реакции на мышиные события надо ввести в main следующие строки и, конечно, написать функции обработки
auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSELOC,OnLMouseMove);
auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSELOC,OnRMouseMove); auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSEDOWN,OnButtonDown);
auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSEDOWN,OnButtonDown);
Обратите внимание,
что дих разделяет WM_MOUSEMOVE на две (в общем случае на три) кнопки. Это нам
как раз подходит, так как мы хотим левой кнопкой вращать, а правой удалять-приближать
(делать zooming) изображение. События отпускания кнопок нам не понадобились
по причине того, что обработчики AUX_MOUSELOC (читай WM_MOUSEMOVE) вызываются
только в случае, если соответствующие кнопки нажаты. Поэтому не нужно поднимать
и отпускать флаг захвата объекта мышью. Именно это нам и нужно. Как легко, когда
кто-то все продумал! Мы не делаем различия между нажатиями левой и правой кнопки,
так как задача у них общая — запомнить текущие координаты указателя мыши. Вставьте
декларации четырех функций, а затем приступим к их созданию. Так как сейчас
мы не имеем классов для инкапсуляции переменных состояния мыши, то придется
добавить глобальные переменные:
int giX,
giY; // Текущая позиция указателя мыши
Тела глобальных
функций обработки вы должны вставить до того места, в котором они вызываются.
Алгоритм изменения параметров gdAngleX, gdAngleX и gdTransZ очевиден, но обратите
внимание на детали. Например, как добывать координаты курсора мыши. Их присылает
система, a AUX хранит их в структуре data, информацию о которой вы можете получить
разве что в файле заголовков Glaux.h:
static void _stdcall OnButtonDown(AUX_EVENTREC *pEvent)
{
//====== Запоминаем
координаты мыши
giX = pEvent->data[AUX_MOUSEX];
giY = pEvent->data[AUX_MOUSEY];
}
static void _stdcall OnLMouseMove(AUX_EVENTREC *pEvent)
{
//====== Узнаем
текущие координаты
int x
= pEvent->data[AUX_MOUSEX];
int у
= pEvent->data[AUX_MOUSEY];
//====== Изменяем
углы поворота пропорционально
//====== смещению
мыши
gdAngleX += (у - giY)/10.f;
gdAngleY += (x
- giX)/10.f;
//====== Запоминаем
координаты мыши
giX = x; giY
= у; >
Static void
_stdcall OnRMouseMove(AUX_EVENTREC *pEvent)
int x = pEvent->data[AUX_MOUSEX];
int у
= pEvent->data[AUX_MOUSEY] ;
//=====<=
На сколько удалить или приблизить
double dx
= (x - giX)/200.f;
double dy
= (y - giY)/200.f;
//====== Удаляем
или приближаем
gdTransZ += (dx
+ dy)/2.f;
//====== Запоминаем
координаты мыши
giX = x; giY = y;
}
Запустите
и опробуйте. Кубик должен управляться, но в обработке мышиных событий присутствует
явная ошибка. Для того чтобы ее увидеть, нажмите правую кнопку и выведите курсор
мыши за пределы окна влево. Изображение исчезло. один из слушателей наших курсов
(Халип В. М. E-mail: viktor@mail.ru) самостоятельно нашел объяснение этому казусу
и устранил дефект. Для того чтобы обнаружить его, вставьте в список директив
препроцессора еще одну — #include <stdio.h>, а в функцию OnRMouseMove
— вызов printf ("\n%d",x);. Теперь координата курсора мыши будет выводиться
в текстовое окно консольного приложения. Повторите опыт с правой кнопкой и убедитесь
в том, что при выходе за пределы окна (влево), координата х получает недопустимое
значение (>65000). Для устранения дефекта достаточно заменить строки:
int x = pEvent->data[AUX_MOUSEXJ;
int у
= pEvent->data[AUX_MOUSEY];
на
short x = pEvent->data[AUX_MOUSEX];
short
у = pEvent->data[AUX_MOUSEY];
в функциях
OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя
через границу окна, координата х изменяется монотонно и приобретает отрицательные
значения. Чтобы быть последовательным, замените тип глобальных данных для хранения
текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;.
Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.
В настоящий
момент перерисовка изображения во время манипуляций мышью очень плохая, так
как мы работаем с одним (front) буфером. Пора подключать
второй. Вместо вызова glFlush; вставьте вызов функции auxSwapBuffers();
J- из другой
библиотеки, которая, как вы помните, не документирована. Но этого мало — надо
заменить волшебное слово SINGLE на не менее волшебное слово —DOUBLE.
Местоположение вычислите самостоятельно. Поиск места вынуждает прокручивать
в голове последовательность вызовов функций, что является полезным, а для многих
и необходимым упражнением. После этого запустите приложение и отметьте, что
управляемость кубика улучшилась, но при достаточно большом его повороте вокруг
оси Y поворот вокруг оси X ведет себя так, как будто сама ось «повернута». Если
вы поменяете порядок вызова двух функций вращения glRotated, то эффект останется,
но проявит себя в симметричном варианте. Исправьте это, если хотите. Хорошая
задача на сообразительность, так как не требует специфических знаний языка программирования,
а только общих представлений о сути преобразований и возможностях библиотек
OpenGL.
В примерах
MSDN можно найти способ введения реакций на нажатия клавиш. Используем клавиши
стрелок для смещения объекта в плоскости Z = const. Введите в функцию main декларацию
4 обработчиков:
auxKeyFunc(AUX_DOWN, KeyDown);
auxKeyFunc(AUX_UP, KeyUp);
auxKeyFunc(AUX_LEFT, KeyLeft);
auxKeyFunc(AUX_RIGHT,
KeyRight);
Теперь по аналогии с мышиными событиями создайте самостоятельно функции обработки и меняйте внутри них те переменные, от которых зависит трансляция изображения. Например:
void
_stdcall KeyDown()
{
gdTransY -=0.1; // Сдвигаем изображение вниз
}
void _stdcall
KeyUp()
{
gdTransY += 0.1; // Сдвигаем изображение вверх
}
void _stdcall KeyLeft()
{
gdTransX -=0.1; // Сдвигаем изображение влево
}
void _stdcall KeyRight()
{
gdTransX +=0.1; // Сдвигаем изображение вправо
}
При тестировании
результата обратите внимание на поведение изображения. Например, чем больше
сдвиг вправо, тем лучше видна левая боковая грань. Кажется, что совместно с
перемещением объекта он поворачивается. Но это не так. Эффект объясняется особенностями
перспективной проекции.
С кубиком
быстро расправляется любой компьютер и видеокарта, а вот с более сложными объектами
могут возникнуть затруднения, бороться с которыми можно
с помощью нескольких приемов. Один из них — использование заранее откомпилированных
списков команд OpenGL. Для иллюстрации этого приема создайте отдельную глобальную
функцию:
void DrawScene()
{
//====== Создаем
новый список команд OpenGL
glNewList(I,GL_COMPILE);
//====== Сюда
поместите код, рисующий куб,
//====== начиная
со строки
static float
v[8][3] =
//====== и заканчивая
for (int
j = 0; j < 4; j++)
glVertex3fv(v[id[i] [j] ] ) ;
}
glEnd() ;
glEndList () ;
}
Список рисующих
команд OpenGL ограничивается операторными скобками вида:
glNewList(I,
GL_COMPILE);
//====== Здесь
располагаются команды OpenGL
glEndList ()
;
Первый параметр
glNewList (типа GLuint) идентифицирует список с тем, чтобы разработчик мог одновременно
использовать несколько списков и вызывать их в нужные моменты времени по номеру.
Вызов нашего (единственного) списка мы будем производить командой glCallList(l);.
Команды, расположенные между строками giNewList(l, GL_COMPILE); и glEndListQ;,
будут откомпилированы и сохранены в списке номер один. В функции перерисовки
их следует просто воспроизвести. Для этого замените существующую версию функции
OnDraw на новую:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT
| GL_DEPTH_BUFFER_BIT);
glMatrixMode
(GL_MODELVIEW);
glLoadldentity
();
glTranslated(gdTransX,
gdTransY, gdTransZ);
glRotated(gdAngleY,
0.,1.,0.);
glRotated(gdAngleX,
1. ,0 .,0 .);
//====== Воспроизводим
команды из списка 1
glCallList (1);
auxSwapBuffers();
}
Вызов DrawScene
можно осуществить внутри функции initQ, то есть один раз за время существования
программного модуля:
void Init ()
{
glClearColor
(1., 1., 1., 0.);
//====== Включаем
интерполяцию цветов полигона
glShadeModel
(GL_SMOOTH);
glShadeModel
(GL_DEPTH_TEST) ;
glPolygonMode(GL_FRONT_AND_BACK,
GL_FILL),
glEnable(GL_LIGHTING);
glEnable(GL_LIGHTO);
glEnable(GL_COLOR_MATERIAL);
//====== Готовим
сцену
DrawScene ()
;
}
Вы можете
запустить новый вариант приложения и убедиться в том, что он работает не хуже
предыдущего. Кроме подготовки изображения (вызов DrawScene) мы внутри init включили
возможность интерполяции цвета точек при заполнении полигона
glShadeModel
(GL_SMOOTH) ;
Этого можно
было и не делать, так как она включена по умолчанию. Но мы вставили код, с тем
чтобы явно обозначить намерение исследовать эту возможность. Для того чтобы
интерполяция начала действовать, надо задать разные цвета разным вершинам одного
полигона. Уберите завершающий фрагмент функции DrawScene, где приведены коды
непосредственного рисования куба, и вставьте вместо него следующий текст:
glPolygonMode (GL_FRONT_AND_BACK, GL_FILL) ;
glBegin (GL_QUADS)
;
//====== Обновляем
генератор случайных чисел
srand (time (0)
) ;
//====== 6 граней
куба
for (int
i = 0; i < 6; i++) ( glNormalSdv (norm[i] ) ;
//====== 4
вершины одной грани
for (int j = 0; j < 4; j++)
{
//====== Задаем
различные цвета
glColorSd (rand()%10/10.,
rand()%10/10., rand()%10/10.) ;
glVertex3fv(v[id[i]
[ j ] ] ) ;
}
}
glEnd() ;
glEndList ()
;
Включите в
начало файла директиву препроцессора:
#include
<time.h>
для того чтобы стала доступной функция timeQ. Она помогает настроить генератор псевдослучайных чисел так, чтобы при разных запусках программы получать различные комбинации цветов. Двойное деление на 10 (rand()%10/10.) позволяет масштабировать и нормировать компоненты цвета. Запустите и проверьте качество интерполяции цветов.
Для иллюстрации работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола смещены (повернуты) на л/5 относительно углов потолка.
Икосаэдр имеет
20 треугольных граней и 12 вершин (1 + 5 на потолке и 1 + 5 на полу). Благодаря
своей правильности он может быть задан с помощью всего лишь двух чисел, которые
лучше вычислить один раз и запомнить. Этими числами является косинус и синус
угла в три пятых окружности, то есть
static
double
//====== atan(l.)
- это пи/4
angle = 3. *
atan(1.)/2.5, //====== 2 характерные точки
V = cos(angle),
W = sin(angle);
Этот код мы
вставим внутрь функции рисования, чтобы не плодить глобальные переменные и не
нарываться на конфликты имен. Вот новая версия функции DrawScene:
void
DrawScene() { static double
//====== 2 характерные
точки
angle = 3. *
atan(l.)/2.5, V = cos(angle), W = sin(angle),
//=== 20 граней икосаэдра, заданные индексами вершин
static GLuint
id[20][3] =
(0,1, 4), (8,1,10),
(7,3,10), (6,10,1), |
(0,4, 9), (8,10,3),
(7,10,6), (9,11,0), |
(9,4, 5), (5,8,
3), (7,6,11), (9,2,11), |
(4,8, 5), (5,3,
2), (11,6,0), (9,5, 2), |
(4,1,8), (2,3,7),
(0,6,1), (7,11,2) |
||
//====== Начинаем
формировать список команд
glNewList (1,GL_COMPILE)
;
//====== Выбираем
текущий цвет рисования
glColor3d (1., 0.4, 1 . ) ;
glBegin (GLJTRIANGLES)
;
for (int i = 0; i < 20; i++)
{
//====== Грубый
подход к вычислению нормалей
glNorma!3dv(v[id[i]
[0] ] ) ;
glVertex3dv(v[id[i]
[0] ] ) ;
glNorma!3dv(v[id[i] [1] ] ) ;
glVertex3dv(v[id[i]
[1] ] ) ;
glNorma!3dv(v[id[i] [2] ] ) ;
glVertex3dv(v[id[i]
[2] ] ) ;
}
glEnd() ;
//====== Конец
списка команд
glEndList ();
}
Точное
вычисление нормалей
Проверьте результат и обсудите качество. В данном варианте нормали в вершинах заданы так, как будто изображаемой фигурой является сфера, а не икосаэдр. Это достаточно грубое приближение. Если поверхность произвольного вида составлена из треугольников, то вектор нормали к поверхности каждого из них можно вычислить точно, опираясь на данные о координатах вершин треугольника. Из $ курса векторной алгебры вы, вероятно, помните, что векторное произведение двух векторов а и b определяется как вектор п, перпендикулярный к плоскости, в которой лежат исходные векторы. Величина его равна площади параллелограмма, построенного на векторах а и b как на сторонах, а направление определяется так, что векторы a, b и п образуют правую тройку. Последнее означает, что если представить наблюдателя на конце вектора п, то он видит поворот вектора а к вектору b, совершаемый по кратчайшему пути против часовой стрелки. На рис. 6.4. изображена нормаль п (правая тройка) при различной ориентации перемножаемых векторов а и b.
Рис. 6.2. Ориентация вектора нормали
Если координаты
векторов а и b известны, то координаты нормали вычисляю по следующим формулам.
Длина вектора нормали п зависит от длин вектор сомножителей и величины угла
между ними:
Nx=AxBz-AzBy
Ny=AzBx-AxBz
Nz=AxBy-AyBx
Можно
потерять много времени на осознание того факта, что не только правление нормали,
но и ее модуль влияют на величину освещенности (и та) вершины, так как сопровождающая
документация (Help) не содер; явных указаний на это. Отметьте также, что цвета
вершин полигона влияю цвета точек заполнения полигона, так как цвета вновь генерируемых
то интерполируются, то есть принимают промежуточные значения между з чениями
цвета вершин.
Чтобы нивелировать
зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют
(или нормируют), то есть делают его длину р; ной единице, оставляя неизменным
направление. С учетом сказанного создал две вспомогательные функции. Первая
масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм
вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:
//====Нормирование
вектора нормали (или любого другого)
void Scale(double
v[3])
{
double d
= sqrt(v[0]*v[0]+v[l]*v[l]+v[2]*v[2]);
if (d
== 0.)
{
MessageBox(0,"Zero
length vector","Error",MB_OK);
return;
}
void getNorm(double vl[3], double v2[3], double out[3])
{
//===== Вычисляем
координаты вектора нормали
//====== по формулам
векторного произведения
out[0] = vl[l]*v2[2] - vl[2]*v2[l];
out[l] = vl[2]*v2(0] - vl[0]*v2[2] ;
out[2] =vl[0]*v2[l] - vl[l]*v2[0];
Scale(out);
}
Замените функцию
DrawScene. В новом варианте мы аккуратно вычисляем и масштабируем нормали в
каждом из двадцати треугольников поверхности икосаэдра:
void DrawScene()
{
static double
angle - 3. *
atanfl.)/2.5, V = cos(angle), W = sin(angle),
v[12] [3] = {
{-V,0.,W},
{V,0.,W}, {-V,0.,-W},
{V,0.,-W}, {0.,W,V},
{0.,W,-V},
{0.,-W,V}, {0.
,-W,-V}, {W,V, 0.},
{-W,V,0.}, {W,-V,0.},
{-W,-V,0.}
};
static GLuint
id[20][3] = {
(0,1, 4), {0,4,
9}, (9,4, 5), (4,8, 5}, (4,1,8),
(8,1,10), (8,10,3),
(5,8, 3), (5,3, 2), (2,3,7),
(7,3,10), (7,10,6),
(7,6,11), (11,6,0), (0,6,1),
(6,10,1), (9,11,0),
(9,2,11), (9,5, 2), (7,11,2) 1;
glNewList(l,GL_COMPILE); glColorSd (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int i = 0; i < 20; i++)
{
double dl[3],
d2[3], norm[3];
for (int
j = 0; j < 3; j++)
{
dl[j] =v[id[i][0]] [j] -v[id[i][l]J [j];
d2[j] =v[id[i][l]]
[j] -v[id[i][2J] [j];
}
//====== Вычисление
и масштабирование нормали
getNorm(dl, d2,
norm);
glNormal3dv(norm);
glVertexSdv(v
[ id[i] [1]]);
glVertex3dv(v[id[i]
[1] ] glVertex3dv(v[id[i] [2] ]
glEnd() ;
}
glEndList () ;
}
Функцию нормировки всех нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE, но обычно это ведет к замедлению перерисовки и, как следствие, выполнения приложения, если изображение достаточно сложное. В нашем случае оно просто, и поэтому вы можете проверить действие настройки, если вставите вызов glEnable (GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному состоянию.
Для того чтобы из существующей заготовки — икосаэдра из двадцати граней — создать сферу, круглую, блестящую и без изъянов, нужно осуществить предельный переход, как в матанализе, бесконечно увеличивая число треугольников при бесконечном уменьшении их размеров. В дискретном мире нет места предельным переходам, поэтому вместо бесконечного деления надо ограничиться каким-то конечным числом и начать делить каждый из двадцати треугольников икосаэдра на все более мелкие правильные треугольники. Вычисление нормали при этом упрощается, так как при приближении к шару нормаль в каждой вершине треугольника приближается к нормали поверхности шара. А последняя равна нормированному вектору радиуса текущей точки. Алгоритм деления проиллюстрируем рисунком (рис. 6.3).
Рис. 6.3. Деление треугольника икосаэдра
Треугольник
с вершинами VI, V2 и V3 разбивается на четыре треугольника: (V1,V12,V31), (V2,V23,V12),
(V3,V32,V23) и (V12.V23.V31). После этого промежуточные точки деления надо посадить
на поверхность шара, то есть изменить их координаты так, чтобы концы векторов
(V12, V23 и V31) дотянулись до поверхности шара. Для этого достаточно нормировать
векторы с помощью уже существующей процедуры Scale. Она впоследствии будет использована
как для масштабирования нормали, так и для нормировки координат вершин новых
треугольников. Но
сейчас мы будем вычислять нормаль приближенно. Введем еще две вспомогательные
функции:
//=== Команды OpenGL для изображения одного треугольника
void setTria(double *vl, double *v2, double *v3)
{
//====== Нормаль
и вершина задаются одним вектором
glNormal3dv(vl);
glVertex3dv(vl);
glNormalSdv (v2);
glVertex3dv(v2);
glNormal3dv(v3);
glVertex3dv(v3);
glEnd() ;
}
//====== Генерация
внутренних треугольников
void Split(double *vl, double *v2, double *v3)
{
//====== Промежуточные
вершины
double v!2[3],
v23[3], v31[3);
for (int
l=0; l< 3; i++) {
//====== Можно
не делить пополам,
//====== так
как будем нормировать
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[i];
v31 [i] = v3[i]+vl [i];
}
//====== Нормируем
три новые вершины
Scale(v!2);
Scale(v23);
Scale(v31); //======
и рисуем четыре треугольника
setTria(vl, v!2, v31);
setTria (v2, v23, v!2);
setTria(v3, v31, v23);
setTria(v!2,v23, v31);
}
Вставьте эти
глобальные функции в файл и дайте следующую версию функцию DrawScene, в которой
отсутствует вызов функции getNorm для точного вычисления нормали, но есть вызов
функции Split для каждой из 20 граней икосаэдра. В результате мы получаем фигуру
из 80 треугольных граней, которая значительно ближе к сфере, чем икосаэдр:
void DrawScene()
{
static
double
angle = 3. *
atan(l.)/2.5, V = cos (angle), W = sin (angle),
v[12] [3] =
{-V,0.,W}, {V,0.,W},
{-V,.0.,-W},
(V,0.,-W), {0.,W,V},
{0.,W,-V},
(0.,-W,V), (0.,-W,-V),
{W,V,0.},
{-W,V,0.}, {W,-V,0.},
{-W,-V,0.}
};
static GLuint id[20][3] =
{
(0,1, 4), (0,4,
9), {9,4, 5}, (4,8, 5), (4,1,8),
(8,1,10), (8,10,3),
(5,8, 3), (5,3, 2), (2,3,7),
(7,3,10), (7,10,6),
(7,6,11), (11,6,0), (0,6,1),
(6,10,1), (9,11,0), (9,2,11), (9,5, 2), (7,11,2)
};
glNewList(l,GL_COMPILE);
glColor3d (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int
i = 0; i < 20; i++)
Split (v[id[i][0]],
v[id[i][l]], v[id[i] [2] ]) ;
glEnd() ;
glEndList () ;
}
На этой стадии
я рекомендую посмотреть, какие интересные и неожиданные результаты могут быть
получены вследствие ошибок. Все мы ошибаемся, вот и я так долго возился с направлением
обхода и со знаком нормали, что в промежуточных вариантах получал чудовищные
комбинации. Многие из них «канули в Лету», но один любопытный вариант легко
смоделировать. Если ошибки происходят в условиях симметричного отражения, то
возникают ситуации, сходные со случайными изменениями узоров в калейдоскопе.
Замените на обратные знаки компонентов вектора в функции Scale. Это действие
в предыдущих версиях программы было эквивалентно изменению знака нормали. Найдите
строку, похожую на ту, что приведена ниже, и замените знаки так, как показано,
на минусы.
v[0] /= -d; v[l]
/= -d; v[2] /= -d;
Выбор способа вычисления нормалей
Верните знаки
на место и продолжим процесс приближения к шару. Прежде всего восстановим возможность
точного вычисления нормалей и дадим пользователю возможность интерактивного
выбора между приближенным и точным их вычислением. С этой целью введем глобальную
переменную
//====== Флаг
способа вычисления нормалей
bool gbSmooth
= false;
которая будет
помнить текущий способ вычисления нормалей, и сделаем так, чтобы каждое нажатие
клавиши N инвертировало эту переменную и способ вычисления нормали. Введите
в функцию main реакцию на нажатие клавиши N, вставив строку
auxKeyFunc(AUX_n,
KeyN);
Реализацию
функции обработки вставьте до функции main:
void
_stdcall KeyN()
{
//====== Изменяем
способ вычисления нормалей
gbSmooth = !gbSmooth;
11====== Заново
создаем список команд
DrawScene();
}
Введите новую
версию функции setTria, которая учитывает выбранный способ вычисления нормалей:
void setTria(double *vl, double *v2, double *v3)
{
glBegin(GLJTRIANGLES);
//====== Если
выбран способ точного вычисления нормали
if (!gbSmooth)
{
//====== Правая
тройка векторов
double dl[3],
d2[3], norm[3];
//====== Вычисляем
координаты векторов
//====== двух
сторон треугольника
for (int
j = 0; j.< 3; j++)
{
dl[j] = vl[j]
- v2[j); d2[j] = v2[j] - v3[j];
}
//====== Вычисляем
нормаль к плоскости
//====== треугольника
со сторонами dl и d2
getNorm(dl, d2,
norm);
glNormalSdv(norm);
glVertex3dv(vl);
glVertex3dv(v2);
glVertex3dv(v3);
}
else
{
//=== Неточное
(приближенное) задание нормали
glNorma!3dv(vl);
glVertexSdv(vl);
glNorma!3dv(v2);
glVertex3dv(v2);
glNorraalSdv(v3);
glVertex-3dv(v3);
}
glEnd ();
}
Запустите и проверьте результат, нажимая клавишу N. Надеюсь, что теперь важность точного вычисления нормалей стала для вас еще более очевидной.
Добавим возможность
дальнейшего деления треугольников и образования фигур, приближающихся к сфере.
Осуществление предельного перехода возможно в математике непрерывных величин,
но невозможно в дискретной математике и тем более в реализациях ее алгоритмов,
где все множества должны быть конечными. Однако мы можем применить конечную
рекурсию для дальнейшего деления треугольников. Замените функцию split на ее
рекурсивную версию. Вы, конечно, помните, что рекурсивная функция вызывает сама
себя до тех пор, пока не выполнится некоторое условие. Здесь условием выхода
из цепи рекурсивных вызовов является равенство нулю последнего параметра depth,
который определяет текущую глубину рекурсии:
void Split(double *vl, double *v2, double *v3,long depth)
{
double
v12[3], v23[3], v31[3];
if (depth == 0)
{
//====== Рисование
наименьших треугольников
setTria(vl, v2,
v3);
//====== и выход
из цепи рекурсивных вызовов
return;
}
//====== Разбиение
треугольника
for (int i = 0; i < 3; i++)
{
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[ij;
v31[i] = v3[i]+vl[i];
}
//====== Дотягивание
до сферы
Scale(v12);
Scale(v23);
Scale(v31); //======
Рекурсивное разбиение на
//====== четыре
внутренних треугольника
Split(vl, v!2,
v31, depth-1);
Split(v2, v23,
v12, depth-1);
Split(v3, v31, v23, depth-1);
Split(v!2, v23, v31, depth-1);
}
Внесите также
изменение в ту строку программы, где происходит вызов Split. Надо добавить параметр,
задающий глубину рекурсии. Если функцию вызвать с нулевой глубиной, то получим
икосаэдр, если увеличивать глубину, то будем получать фигуры, более близкие
к шару:
for (int
i = 0; i < 20; i++)
Split (v[id[i)
[0]], v[id[i][l]], v[id[i] [2]], 3);
Запустите и проверьте, нажимая клавишу N. Попробуйте изменить глубину рекурсии, только не переусердствуйте. Если задать глубину более 10, то можно не дождаться ответа. Рекурсия дорого стоит, поэтому исследованный подход абсолютно неприемлем для создания сферы. Аналогичный вывод справедлив для других объемных изображений, создаваемых с помощью задания вершин большого числа геометрических примитивов.
В данный момент для иллюстрации процесса приближения изображаемой фигуры к сфере напрашивается такой сценарий: пользователь нажимает клавишу — пробел, глубина рекурсии изменяется и изображение пересчитывается. Алгоритм управления глубиной рекурсии, очевидно, следует выбрать таким, чтобы, оставаясь в рамках допустимых значений, можно было проходить весь диапазон в обе стороны. Введите в функцию main обработку нажатия клавиши пробела:
auxKeyFunc(AOX_SPACE,
KeySpace);
и создайте функцию
обработки:
void _stdcall KeySpace()
{
//====== Флаг
роста числа разбиений
static bool
bGrow = true;
//====== Продолжаем
разбивать до глубины 4
if (bGrow SS giDepth < 4)
{
giDepth += 1;
}
//====== Смена
знака при глубине 4
else if (giDepth > 0)
{
bGrow = false;
giDepth == 1;
}
//====== Смена
знака при глубине О
else
{
bGrow = true;
giDepth += 1;
}
DrawScene () ;
}
Алгоритм предполагает,
что глобально определена переменная giDepth, которая хранит текущее значение
глубины рекурсии. Добавьте к существующим глобальным переменным объявление:
//====== Глубина
рекурсии
int giDepth;
В функции
DrawScene замените параметр 3 (при вызове Split) на giDepth и запустите на выполнение.
Не
знаю, как объяснить, но в Visual Studio б этот код почему-то работает, не-—
смотря на явный промах, который типичен не только для начинающих программистов.
Опытный читатель, конечно же, заметил, что мы создаем новые списки изображений,
не уничтожая старые. Такие действия классифицируются как утечка памяти (memory
lickage). Для ее устранения вставьте следующий фрагмент в функцию DrawScene
перед вызовом glNewList:
//====== Если
существует 1-й список,
if (gllsList(1))
//====== то освобождаем
память
glDeleteLists
(1,1);
Разъяснения
можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при
выборе места вставки фрагмента, так как операции с памятью имеют особую важность.
Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения,
которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный
облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние
точного вычисления нормалей.
Массивы вершин, нормалей и цветов
Неэффективность
алгоритма последовательного рисования большого числа примитивов не является
тайной для тех, кто имеет дело с трехмерной графикой. Поэтому в технологии OpenGL
существует ряд приемов (поддержанных функциями), которые основаны на использовании
заранее заготовленных массивов, а также списков команд OpenGL. Они значительно
повышают эффективность работы конвейера при передаче (rendering) изображений,
составленных из десятков и сотен тысяч примитивов. Например, функция glDrawElements
позволяет задать геометрические примитивы экономичным способом, то есть с минимальными
затратами на вызовы функций. До сих пор мы вызывали в среднем 4-5 функций для
каждого треугольника. При этом многократно повторялись, так как вершины, общие
для смежных треугольников, задавались не один раз. Массивы величин, ассоциируемых
с вершинами (координаты, нормали, цвета и другие), могут быть сформированы заранее
и использованы при описании геометрии с помощью массива индексов. Функция glDrawElements
требует в качестве одного из параметров массив индексов вершин полигонов. Вот
ее прототип:
void
glDrawElements (GLenum mode,
GLsizei count,
GLenum type,
const GLvoid *indices);
Функция конструирует
count элементов типа mode. Параметр indices должен содержать адрес массива индексов,
который формируется заранее. Параметр type определяет тип элементов массива
indices. Он может принимать одно из трех фиксированных значений: GL_UNSIGNED_BYTE
(используется 8-битовый индекс), GL_UNSIGNED_SHORT (16-биТНЫЙ ИНДСКС), GL_UNSIGNED_INT
(32-биТНЫЙ). Характерной особенностью рассматриваемой технологии является то,
что величины, ассоциируемые с каждой вершиной примитива, могут храниться в разных
массивах или в одном массиве структур с разными полями. Они задаются с помощью
6 функций:
Другой массив
индексов — indices, определяет порядок выбора элементов из этих шести массивов.
Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL.
Для перевода ее в режим использования массивов надо несколько раз вызвать функцию
glEnableClientstate. Каждый вызов включает
один из шести рассмотренных режимов. Только после этого функция glDrawElements
способна эффективно задать сразу все примитивы. Например, вызов:
glEnableClientState(GL_VERTEX_ARRAY);
включает режим
использования массива координат вершин, а вызов этой же функции с параметром
GL_NORMAL_ARRAY включает использование массива нормалей.
Совместно с командой glDrawElements обычно используют тот способ повышения эффективности отображения примитивов, который мы уже используем. Речь идет о паре функций: glNewList, glEndList. Все команды OpenGL, заданные между вызовами этих двух функций, оптимизируются, компилируются (по выбору) и запоминаются в отдельном нумеруемом списке.
Для иллюстрации
рассматриваемых возможностей мы создадим сферу, составленную из треугольников,
но при этом не будем отталкиваться от какого-либо правильного многогранника,
а используем модель глобуса. Количество и пропорции треугольников будут зависеть
от количества геодезических линий на сфере (параллелей и меридианов). Если вы
посмотрите на рис. 6.4 или представите себе глобус, то согласитесь с тем, что
параллели и меридианы разбивают поверхность сферы на множество сферических четырехугольников.
Исключение составляют лишь полюса, вокруг которых мы имеем сферические треугольники.
Если затем каждый сферический четырехугольник разделить диагональю, то он даст
два сферических треугольника.
Рис.
6.4. Разбиение сферы на треугольники
Мы будем управлять
степенью дискретизации сферы с помощью двух чисел: количества колец (gnRings)
и количества секций (gnSects). Они определяют как полное количество вершин,
так и треугольников. Если глобально зададим переменные:
const UINT gnRings = 20; // Количество колец (широта)
const
UINT gnSects = 20; // Количество секций (долгота),то,
так как каждый прямоугольник разбит на два треугольника, общее количество треугольников
будет:
const UINT
gnTria = (gnRings+1) * gnSects * 2;
//===Нетрудно
подсчитать и общее количество вершин:
const UINT
gnVert = (gnRings+1) * gnSects + 2;
Мы уже, по
сути, начали писать код, поэтому создайте новый файл Sphere.срр и подключите
его к проекту, а предыдущий файл OG.cpp отключите. Эти действия производятся
так:
Введите в
него директивы препроцессора, которые нам понадобятся, а также объявления некоторых
констант:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <gl\glaux.h>
const UINT gnRings = 40; // Количество колец (широта)
const UINT
gnSects = 40; // Количество секций (долгота)
//====== Общее
количество треугольников
const UINT
gnTria = (gnRings+1) * gnSects * 2;
//====== Общее
количество вершин
const UINT
gnVert = (gnRings+1) * gnSects + 2;
//====== Два
цвета вершин
const COLORREF gClrl = RGB(0, 255, 0);
const COLORREF
gClr2 = RGB(0, 0, 255);
const double
gRad = 1.5; // Радиус сферы
const double
gMax =5.; // Амплитуда сдвига
const double
PI = atan(1.)*4,; // Число пи
Класс
точки в 3D
С каждой вершиной,
как вы помните, связано множество параметров, определяющих качество изображения
OpenGL. Мы остановимся на наборе из трех величин: координаты вершины, вектор
нормали и цвет. Так как вектор нормали и координаты
можно задать с помощью двух объектов одного и того же типа (три вещественных
переменных х, у, z), то целесообразно ввести в рассмотрение такое понятие, как
точка трехмерного пространства. И воплотить его в виде класса CPoint3D, который
инкапсулирует функциональность такой точки. Введите определение класса в конец
файла Sphere. срр:
//======
Точка 3D-пространства
class CPointSD
{
public: float x, у, z; // Координаты точки
// ======
Конструктор по умолчанию
CPoint3D () {
х = у = z = 0; ) //====== Конструктор с параметрами
CPointSD (double cl, double c2, float c3)
{
x = float (cl)
;
z = float(c2)
;
у = float(c3) ;
}
//====== Операция
присвоения
CPoint3D& operator= (const CPoint3D& pt)
{
x = pt.x;
z = pt . z ;
У = Pt.y;
return *this;
//====== Операция
сдвига в пространстве
CPoint3D& operator+= (const CPoint3D& pt)
{
x += pt.x;
y += Pt.y;
z += pt . z ;
return * this ;
}
//====== Конструктор
копирования
CPointSD (const CPoint3D& pt)
{
*this = pt;
}
};
Обратите внимание
на тот факт, что конструктор копирования использует код уже существующей операции
присвоения. Имея в своем распоряжении класс CPointSD, мы можем создать еще один
тип данных — структуру, поля которой объединяют все величины, связанные с вершиной
треугольника. Массив данных такого типа будет хранить информацию обо всех вершинах
изображения и при этом не будет повторений:
//====== Данные
о вершине геометрического примитива
struct VERT
{
CPointSD v; //
Координаты вершины
CPoiivt3D n; // Координаты нормали
DWORD с; // Цвет вершины
};
Введите эту
декларацию после кода, определяющего CPoint3D. Как было отмечено, функция glDrawElements
в качестве параметра требует задать массив индексов вершин. В соответствии с
этими индексами вершины треугольников будут выбираться из общего массива вершин.
Порядок следования индексов зависит от порядка обхода вершин при задании треугольников.
Как вы помните, он должен идти против часовой стрелки, если смотреть на примитив
с конца внешней нормали. В этом случае знак нормали соответствует формулам векторной
алгебры,!: которые мы уже рассматривали.
Будет удобно,
если мы сначала создадим структуру, которая объединяет три индекса вершин одного
треугольника. Тогда массив структур такого типа сможет играть роль массива индексов,
требуемого функцией glDrawElements. Введите следующее описание в продолжение
файла:
struct TRIA
{
//====== Индексы
трех вершин треугольника,
//====== выбираемых
из массива вершин типа VERT
//====== Порядок
обхода — против часовой стрелки
int i1;
int
i2;
int i3;
};
Далее нам
понадобятся две глобальные неременные типа CPointSD, с помощью *":'
которых мы будем производить анимацию изображения сферы. Анимация, а также
различие цветов при задании вершин треугольников позволят более четко передать
трехмерный характер изображения. Наличие освещения подвижного объекта также
заметно увеличивает его реалистичность. При создании програм-| мы мы обойдемся
одним файлом, поэтому новые объявления продолжайте вставлять в конец файла Sphere.срр:
//====== Вектор
углов вращения вокруг трех осей ?
CPointSD gSpin;
//====== Вектор случайной девиации вектора gSpin
CPointSD gShift;
При каждой
смене буферов (перерисовке изображения) мы будем вращать изоб- ; ражение сферы
вокруг всех трех осей на некоторый векторный квант gshif t. Для того чтобы вращение
было менее однообразным, введем элемент случайности. Функция Rand, приведенная
ниже, возвращает псевдослучайное число в диапазоне (-х, х). Мы будем пользоваться
этим числом при вычислении компонентов вектора gshif t. Последний, воздействуя
на другой вектор gSpin, определяет новые значения трех углов вращения, которые
функция glRotate использует для задания очередной позиции сферы:
inline double Rand(double x)
{
//====== Случайное
число в диапазоне (-х, х)
return х - (х + х) * rand() / RAND_MAX;
}
Учитывая сказанное,
можно создать алгоритм перерисовки:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT)
;
//=== Сейчас
текущей является матрица моделирования
glLoadldentityО;
//====== Учет
вращения
glRotated(gSpin.х, 1., О, 0.) ;
glRotated(gSpin.y, 0., 1., 0.);
glRotated(gSpin.z,
0., 0., 1.) ;
//====== Вызов
списка рисующих команд
glCallList(1);
//====== Подготовка
следующей позиции сферы
gSpin += gShift;
//===== Смена буферов auxSwapBuffers();
}
Подготовка
сцены
Изображение
сферы целесообразно создать заранее (в функции init), а затем воздействовать
на него матрицей моделирования, коэффициенты которой изменяются в соответствии
с алгоритмом случайных девиаций вектора вращения. При разработке кода функции
init надо учесть специфику работы с функцией glDrawElements, которая обсуждалась
выше. Кроме того, здесь мы производим установку освещенности, технологию и детали
которой можно выяснить в сопровождающей документации (MSDN). Введите следующие
коды функции инициализации и вставьте их до функции перерисовки:
void Init ()
{
//=== Цвет фона
(на сей раз традиционно черный)
glClearColor
(0., 0., 0., 0.);
//====== Включаемаем
необходимость учета света
glEnable(GL_LIGHTING);
//=== Включаемаем
первый и единственный источник света
glEnable(GL_LIGHT());
//====== Включаем
учет цвета материала объекта
glEnable(GL_COLOR_MATERIAL);
// Вектор для
задания различных параметров освещенности
float v[4]
=
{
0.0Sf, 0.0Sf,
0.0Sf, l.f
};
//=== Сначала
задаем величину окружающей освещенности glLightModelfv(GL_LIGHT_MODEL_AMBIENT,
v);
//====== Изменяем
вектор
v[0] = 0.9f;
v[l] = 0.9f; v[2] = 0.9f;
//====== Задаем
величину диффузной освещенности
glLightfv(GL_LIGHTO,
GL_DIFFUSE, v) ;
//======= Изменяем
вектор
v[0] = 0.6f;
v[l] = 0.6f; v[2] = 0.6f;
//====== Задаем
отражающие свойства материчала
glMaterialfv(GL_FRONT,
GL_SPECULAR, v);
//====== Задаем
степень блесткости материала
glMateriali(GL_FRONT,
GL_SHININESS, 40);
//====== Изменяем
вектор
v[0] = O.f; v[l]
= O.f; v[2] = l.f; v[3] = O.f;
//====== Задаем
позицию источника света
glLightfv(GL_LIGHTO,
GL_POSITION, v);
//====== Переключаемся
на матрицу проекции
glMatrixMode(GL_PROJECTION);
glLoadldentity();
//====== Задаем
тип проекции
gluPerspective(45,
1, .01, 15);
//=== Сдвигаем точку наблюдения, отодвигаясь от
//=== центра сцены в направлении оси z на 8 единиц
gluLookAt (0,
0, 8, 0, 0, 0, 0, 1, 0) ;
//====== Переключаемся
на матрицу моделирования
glMatrixMode(GL_MODELVIEW);
//===== Включаем механизм учета ориентации полигонов
glEnable(GL_CULL_FACE);
//===== Не учитываем обратные поверхности полигонов
glCullFace(GL_BACK);
//====== Настройка
OpenGL на использование массивов
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
//====== Захват
памяти под динамические массивы
VERT *Vert =
new VERT[gnVert];
TRIA *Tria =
new TRIA[gnTria];
//====== Создание
изображения
Sphere(Vert,
Trial;
//====== Задание
адресов трех массивов (вершин,
//====== нормалей
и цветов),
/1====== а
также шага перемещения по ним
glVertexPointer(3, GL_FLOAT, sizeof(VERT), &Vert->v); glNormalPointer(GL_FLOAT, sizeof(VERT), &Vert->n);
glColorPointer(3,
GL_UNSIGNED_BYTE, sizeof(VERT),
SVert->c);
srand(time(0));
// Подготовка ГСЧ
gShift = CPoint3D
(Rand(gMax),Rand(gMax),Rand(gMax));
//====== Формирование
списка рисующих команд
glNewListd, GL_COMPILE);
glDrawElements(GL_TRIANGLES, gnTria*3, GL_UNSIGNED_INT, Tria);
glEndList() ;
//== Освобождение памяти, так как список сформирован
delete [] Vert;
delete [] Tria;
}
Формула
учета освещенности
Семейство
функций glLightModel* позволяет установить общие параметры освещенности сцены.
В частности, первый параметр GL_LIGHT_MODEL_AMBIENT сообщает OpenGL, что второй
параметр содержит четыре компонента, задающие RGBA-интенсивность освещенности
всей сцены. По умолчанию вектор освещенности сцены равен (0.2, 0.2, 0.2, 1.0).
Команда glLight* устанавливает параметры источника света. Мы пользуемся ею два
раза для задания диффузного и рефлективного компонента интенсивности света.
Если вы обратитесь к документации, то увидите, что с помощью glLight* можно
задать еще более десятка параметров источника света. Формулу учета освещения
я нашел в документации лишь в словесном описании, но рискну привести ее в виде
математического выражения.
В режиме RGBA-интенсивность
каждого из трех компонентов цвета освещенной вершины вычисляется как сумма нескольких
составляющих. Первая составляющая учитывает эмиссию света материалом, вторая
— освещенность окружения (ambient) или всей сцены, третья — является суммой
вкладов от всех источников света. Максимально допустимое число источников, как
вы помните, определено константой GL_MAX_LIGHTS, которая в нашем случае равна
8:
L=Me+MaLaf+Сумма(MaLai+MdLdi(N*Vl)+MsLsi(Ve*Vl)^h)
Здесь символ
т обозначает некоторое свойство материала, а символ / — свойство света.
Индекс е в применении к материалу обозначает эмиссию, а в применении
к
вектору v
— eye (глаз). Остальные индексы в применении к материалу обозначают различные
компоненты его отражающих свойств.
Члены в круглых
скобках — это скалярные произведения векторов. Если они дают отрицательные значения,
то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения
устанавливается равным alpha-компоненту диффузного отражения материала. Так
как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить.
Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно
отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших
единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный
вклад. Чем больше их рассогласование, тем меньший вклад даст последний член
формулы.
Ориентация
поверхности
Кроме установки
параметров света код функции init содержит довольно много других установок,
которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно,
вы помните из курса аналитической геометрии, что некоторые поверхности имеет
ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT),
если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной
(BACK), если направление обхода было обратным. В частности, ориентация поверхности
влияет на ориентацию нормали.
Вы
можете реверсировать эту установку, задав режим glfrontFace (GL_CW). По умолчанию
действует установка glFrontFace(GL_CCW). Аббревиатура CW означает clockwise
(по часовой стрелке), a CCW — counterclockwise (против часовой стрелки). Кстати,
вы, вероятно, видели в литературе изображение ленты Мебиуса или бутылки Клейна,
поверхности которых односторонние и поэтому не имеют ориентации.
Команда glEnable (GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она должна сопровождаться одним из флагов, определяющих сторону поверхности, например glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется. Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому эти установки нам вполне подходят.
Массив вершин, нормалей и цветов
Три команды
glEnableClientstate говорят о том, что при формировании изображения будут заданы
три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно
задают адреса этих массивов. Здесь важно правильно задать не только адреса трех
массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся
одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам
одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив
вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически
внутри функции init. Характерно, что после того, как закончилось формирование
списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами,
так как вся необходимая информация уже хранится в списке. Формирование массивов
производится в функции Sphere, которую еще предстоит разработать.
Далее по коду
Init идет формирование списка рисующих команд. Так как массивы вершин и индексов
их обхода при задании треугольников уже сформированы, то список рисующих команд
создается с помощью одной команды glDrawElements. Ее параметры указывают:
Команды:
srandftime(0));
// Подготовка ГСЧ
gShift = CPoint3D(Rand(gMax),
Rand(gMax), Rand(gMax));
позволяют задать характер вращения сферы. Константа const double gMax = 5.;
выполняет роль регулятора (ограничителя) степени подвижности сферы. Если вам захочется, чтобы сфера вращалась более резво, то увеличьте эту константу и перекомпилируйте проект.
Формирование массива вершин и индексов
Самой сложной
задачей является правильное вычисление координат всех вершин треугольников и
формирование массива индексов Tria, с помощью которого команда glDrawElements
обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм
последовательного обхода сначала всех сферических треугольников вокруг полюсов
сферы, а затем обхода сферических четырехугольников, образованных пересечением
параллелей и меридианов. В процессе обхода формируется массив вершин vert. После
этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный
и южный полюса обрабатываются
индивидуально. Для осуществления обхода предварительно создаются константы:
Для упрощения
восприятия алгоритма следует учитывать следующие особенности, связанные с порядком
обхода вершин:
void Sphere(VERT *v, TRIA* t)
{
//====== Формирование
массива вершин
//====== Северный
полюс
v[0].v = CPointSD
(0, gRad, 0);
v[0].n = CPoint3D
(0, 1, 0);
v[0].с = gClr2;
//====== Индекс
последней вершины (на южном полюсе)
UINT last = gnVert
- 1; //====== Южный полюс
v[last].v = CPointSD (0, -gRad, 0);
v[last].n = CPointSD (0, -1, 0) ;
v[last].c = gnVert
& 1 ? gClr2 : gClrl;
//====== Подготовка
констант
double da
= PI / (gnRings +2.),
db = 2. * PI
/ gnSects,
af = PI - da/2.;
bf = 2. * PI
- db/2.;
//=== Индекс
вершины, следующей за северным полюсом
UINT n = 1;
//=== Цикл по
широтам
for (double
a = da; a < af; a += da)
{
//=== Координата у постоянна для всего кольца
double у
= gRad * cos(a),
//====== Вспомогательная
точка
xz = gRad * sin(a);
//====== Цикл
по секциям (долгота)
for (double b = 0.; b < bf; n++, b += db)
}
// Координаты проекции в экваториальной плоскости
double х
= xz * sin(b), z = xz * cos(b);
//====== Вершина,
нормаль и цвет
v[n].v = CPointSD
(x, у, z);
v[n].n = CPointSD (x / gRad, у / gRad, z / gRad);
v[n].c = n &
1 ? gClrl : gClr2; } }
//====== Формирование
массива индексов
//====== Треугольники
вблизи полюсов
for (n = 0; n < gnSects; n++)
{
//====== Индекс
общей вершины (северный полюс)
t[n] .11 = 0;
//====== Индекс
текущей вершины
t[n] .12 = n
+ 1;
//====== Замыкание
t[n].13 = n ==
gnSects - 1 ? 1 : n + 2;
//====== Индекс
общей вершины (южный полюс)
t [gnTria-gnSects+n]
.11 = gnVert - 1;
t tgnTria-gnSects+n]
. 12 = gnVert - 2 - n;
t [gnTria-gnSects+n]
.13 = gnVert - 2
t ( (1 + n) %
gnSects) ;
}
//====== Треугольники
разбиения колец
//====== Вершина,
следующая за полюсом
int k
= 1;
//====== gnSects
- номер следующего треугольника
S' n = gnSects;
for (UINT
i = 0; i < gnRings; i++, k += gnSects) {
for (UINT
j = 0; j < gnSects; j++, n += 2) {
//======= Индекс
общей вершины
t[n] .11 = k
+ j;
//======= Индекс
текущей вершины
t[n].12 = k + gnSects + j;
//======= Замыкание
t[n].13 = k + gnSects + ((j + 1) % gnSects)
//======= To
же для второго треугольника
t[n + 1].11 = t[n].11;
t[n + 1].12 = t[n].13;
t[n + 1J.13 =
k + ((j + 1) % gnSects);
Для завершения
работы осталось дополнить программу стандартным набором процедур, алгоритм функционирования
которых вы уже изучили:
void_stdcall
OnSize(GLsizei w, GLsizei h) { glViewport(0, 0, w, h);
}
void main ()
{
auxInitDisplayMode(AUX_RGB
| AUX_DOUBLE) ;
auxInitPositiondO,
10, 512, 512);
auxInitwindow("Vertex
Array");
Init() ;
auxReshapeFunc (OnSize) ;
auxIdleFunc (OnDraw) ;
auxMainLoop (OnDraw) ;
}
Запустите
проект на выполнение и уберите возможные неполадки. Исследуйте функционирование
программы, вводя различные значения глобальных параметров (регулировок). Попробуйте
задать нечетное число секций. Объясните результат. В качестве упражнения
введите возможность интерактивного управления степенью дискретизации сферы и
исследуйте эффективность работы конвейера при ее увеличении.